Merge train: #9807, #9816 - #9855
Merged
Merged
Conversation
added 3 commits
September 5, 2026 18:40
… filter that admits when it is outgrown `prune_dead_per_object_layout_owners` walked every live key three times per collection — `retain`, `layout_addr_filter_rebuild` (which buffered them all into a `Vec<usize>` first), then `recount_young_layout_records`. The last two want exactly the survivor set `retain` already visits, so they fold into its closure and the `Vec` disappears; on the compiled claude-code TUI that `Vec` alone allocated 50.6 MB per 400-character reply. The new `PERRY_LAYOUT_DIAG` instrument reports what made this expensive: 162,258 live keys against a 4,096-bit address sketch documented for "one or two entries", with all 4,096 bits set. Every probe answers "may hold", so the early returns the sketch exists to serve never fire, and each rebuild is an O(live keys) walk that restores the all-ones state it started from. Past one eighth of the bits the rebuild now reaches that state in O(1) instead. Widening the sketch is a codegen change — the geometry and hash are mirrored in `emit_gated_forget_object_layout` — and would need ~190 KB of inline TLS per thread to discriminate at this occupancy. `transfer_per_object_descriptor` gains the emptiness test its shared flag cannot express: one `len` load instead of two hashes per evacuated object, for a map that is empty for the whole of a cc turn. `LAYOUT_DIAG` is declared with `crate::perry_thread_local!`, as `scripts/check_thread_locals.py` requires of every new declaration. Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
The fused `for…of` advance (`js_for_of_next`, the runtime entry the compiler's
desugar emits) already recycled ONE `{ value, done }` object per ITERATOR for
builtin Map/Set iterators: the result local is a compiler temporary the loop
body cannot name, and the driver reads `done`/`value` out of it before the next
advance, so mutating one cached object is unobservable. Array iterators fell
through to the generic arm and minted a fresh 40-byte object per element.
The allocation-site census of the compiled claude-code TUI attributes 100 % of
its iterator-result bytes — 14.4 MB of a 3300-character reply, 15.5 % of all
attributed arena bytes and the third-largest category — to exactly that:
`array::iter_object` under `js_native_call_method`, one object per element of
every generic `for…of`. A minified bundle reaches it whenever the iterated
value is not a statically proven array, which is nearly always.
So the array iterator takes the same fused arm, and the recycling routine moves
to `iter_result::emit_iter_result_cached` so the two families share ONE
implementation instead of a second copy — the drift #7564 removed from the five
result constructors this module replaced.
Three things the change had to keep intact:
* The override probe still runs first, so a patched own `next` wins on the
fused path exactly as it does on the manual one.
* `node:sqlite`'s `{ done, value }` key order is observable, so the cache is
built with that iterator's own order.
* Field 5 now holds the cache, so `reserved_slot_floor_for_class_id` rises
from 5 to 6 for the array iterator — without that, the first user property
added to an iterator (`it.foo = 1`) would land on the cache field.
Manual `.next()`, spread, `Array.from`, `yield*` and `for await` are unchanged
and keep allocating fresh results.
Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train: #9807, #9816, plus one gate verdict.
LAYOUT_DIAGneeded classifying — every field is a count, a length or a running maximum (prunes,typed_len,masks_len,typed_max,masks_max,filter_bits_*,useful_keys,rebuilt), sonot_a_gc_pointer.On the
pipetestAn earlier run of this pair failed
streams::pipe::tests::pipe_keeps_locks_until_async_abort_settles, so it was held rather than landed. Investigated rather than assumed:main: passesmain+ perf(gc): one pass over the per-object layout tables per prune, and a filter that admits when it is outgrown #9807 alone: passesmain+ perf(runtime): recycle the for-of result object for array iterators (167,946 objects per 400-char claude-code reply) #9816 alone: passesmain+ both together: passesIt is a flake, not a regression. That suite has timing-sensitive async tests that fail under load, and the original run shared the machine with a concurrent build. Running validations serially is the fix; the PRs are clean.
Validation
64/64 lint gates;
perry-runtime,perry-codegen,perry-hir,perry-transformgreen, andperry-stdlibgreen on re-run.